package com.huawei.java.main;



import com.huawei.java.main.deployentity.FakeServer;
import com.huawei.java.main.deployentity.Server;
import com.huawei.java.main.deployentity.Vm;
import com.huawei.java.main.inputentity.Require;
import com.huawei.java.main.inputentity.ServerType;
import com.huawei.java.main.inputentity.VmType;

import java.io.FileNotFoundException;
import java.util.*;
import java.util.function.Predicate;

public class Main {
    static List<Output> outputList = new ArrayList<>();//每天输出列表
    static Output dailyOutPut;

    static List<Vm> VmList =new LinkedList<>();//记录所有已经添加过的虚拟机（方便执行删除操作）
    static List<Server> serverList =new LinkedList<>();//记录所有购买的服务器

    static long totalId;//分配服务器id
    static int currentTotalBuyServerNum=0;//当前总共购买的服务器数量

    public static void main(String[] args) throws FileNotFoundException {

        //本地测试数据
        String pathName = "C:\\Users\\liuhaodong\\Desktop\\huawei-soft-compete\\training-data\\training-2.txt";
        String[] arr = Input.readFile(pathName);
        Input.resolve(arr);

        //标准输入数据
//        Input.readStdIn();

        process();

        //题目要求的最终输出结果
//        for (int i=0;i< outputList.size();i++){
////            System.out.println("=============day "+i+"=============");
//            Output dailyOutPut=outputList.get(i);
//            dailyOutPut.result();
//        }

        //查看统计数据
        showStatistical();

//        for (DeployServer deployServer:deployServerList){
//            int restCpu=deployServer.getNodeARestCPU()+deployServer.getNodeBRestCPU();
//            int restMem = deployServer.getNodeARestMemory()+deployServer.getNodeBRestMemory();
//            int cpu=deployServer.getCPU();
//            int mem=deployServer.getMemory();
//            System.out.println("cpu rate:"+(double)restCpu/cpu+" memory rate:"+(double)restMem/mem);
//        }


    }

    //思路参考：https://blog.csdn.net/weixin_44459972/article/details/114756490
    static void process(){
        //1.将服务器分类，先分为核内比大于1和小于1的
        List<ServerType> greaterThanOnes = new ArrayList<>();//核内比大于一，且按照costPerformance（性价比）升序排序
        List<ServerType> lessThanOnes = new ArrayList<>();//核内比小于等于一
        classifyServer(greaterThanOnes,lessThanOnes);


        //以天为单位遍历
        // TODO: 2021/3/16 暂时只看前10天的，最终应该是i<Input.dailyRequireList.size()
        for (int i=0;i<Input.dailyRequireList.size();i++){
            System.out.println("day:"+i);
            //每天一个新的output
            dailyOutPut =new Output(i,currentTotalBuyServerNum);


            List<Require> delRequireList = Input.dailyRequireList.get(i).get("delList");//每天的删除请求
            List<Require> addRequireList = Input.dailyRequireList.get(i).get("addList");//每天的add请求


            //处理每天的del请求
            for (Require delRequire:delRequireList){
                Vm delVm = null;
                for(Vm d: VmList){//找到该删除请求对应的虚拟机
                    if (d.getVmId()==delRequire.getVmId()){
                        delVm =d;
                        break;
                    }
                }
                Server server = delVm.getDeployServer();//该虚拟机部署到的服务器
                server.releaseVM(delVm);//释放资源
                VmList.remove(delVm);//删除该虚拟机
            }


            //迁移
            int totalMigrateNum = VmList.size()/200;//每天能够迁移的虚拟机上限

            //迁移当前剩余资源最大的服务器
            for (int index=serverList.size()-1;index>=0;index--){
                //剩余资源占比越高越排在后面
                serverList.sort(Comparator.comparingInt(Server::getRestResource));
                Server server = serverList.get(index);
                if (server.getDeployedVMList().size()>totalMigrateNum) break;
                totalMigrateNum-= migrate(server, serverList.subList(0,index));
            }


            //处理每天的add请求
            double averageRatio=averageRequiresRatio(addRequireList);//当日的平均核内比
//            System.out.println(averageRatio);
            //2.将一天的请求分类为单节点部署和双节点部署，这里暂时用第一天的数据做测试
            List<Require> singleNodeList = new ArrayList<>();
            List<Require> doubleNodeList = new ArrayList<>();
            doubleOrSingle(addRequireList,singleNodeList,doubleNodeList);

            //3.分别singleNodeList和doubleNodeList执行购买并部署
            //将每天的add请求分为双节点和单节点的，再根据平均核内比分配相应的服务器。
            //有点分治的思维
            if (averageRatio>1){
                // TODO: 2021/3/16 降序还是升序？在斟酌一下
                //将当日的单、双节点的请求按照cpu降序排序
                singleNodeList.sort((o1, o2) -> o2.getVmType().getCPU()-o1.getVmType().getCPU());
                doubleNodeList.sort((o1, o2) -> o2.getVmType().getCPU()-o1.getVmType().getCPU());
                distribute(doubleNodeList,greaterThanOnes,serverList);//用核内比大于一的服务器
                distribute(singleNodeList,greaterThanOnes,serverList);

            }else {
                singleNodeList.sort((o1, o2) -> o2.getVmType().getMemory()-o1.getVmType().getMemory());
                doubleNodeList.sort((o1, o2) -> o2.getVmType().getMemory()-o1.getVmType().getMemory());
                distribute(doubleNodeList,lessThanOnes,serverList);//用核内比小于一的服务器
                distribute(singleNodeList,lessThanOnes,serverList);

            }


            //每天结束后添加每天的部署和购买服务器信息
            currentTotalBuyServerNum+=dailyOutPut.getDailyDeployServer().size();
            outputList.add(dailyOutPut);


        }
    }

    /**
     * 迁移
     * @param sourceServer 需要迁移的服务器
     * @param availableServers 可迁移的服务器，这些服务器必须是已经购买的
     * @return 消耗的迁移次数
     *
     */
    static int migrate(Server sourceServer, List<Server> availableServers){
        int count=0;
        List<Vm> vmList = sourceServer.getDeployedVMList();//待迁移的虚拟机列表
        if (vmList.size()==0) {
            int i;
        }
        for (int i= vmList.size()-1;i>=0;i--){
            Vm vm= vmList.get(i);
            for (Server targetServer : availableServers){
                if (sourceServer.equals(targetServer)) continue;
                String node = isDeployable(vm, targetServer);
                if (node==null||node.length()==0) continue;//不能部署
                //可以部署
                count++;
                sourceServer.releaseVM(vm);//从原来的服务器上删除该虚拟机
                vm.migrate(targetServer,node);//迁移该虚拟机
                targetServer.addVM(vm);//添加到新的服务器上
                vmList.remove(vm);
                break;
            }
            //如果本轮没有迁移，说明该虚拟机没有匹配的服务器可以部署
            if (i>=vmList.size()) i= vmList.size();
        }
        return count;
    }


    /**
     * 部署虚拟机
     * 外层当天请求列表，不为空进行下面的步骤
     * 内层deployServer列表，如果为空，则新增server
     * 首先从deployServer列表中找到能够部署的节点，这是现有已经购买的服务器，
     * 如果找不到这样的节点，则在serverList中遍历，找到第一个合适的server，加入到deployServer列表中（找第一个是保证了性价比最高）
     * 删除当前处理的require
     *
     * 注意，该方法结束后参数requireList应该为空！即分配完所有的请求！
     * @param requireList 需要处理的请求
     * @param serverTypeList 可选购买的服务器型号
     * @param serverList
     */
    static void distribute(List<Require> requireList, List<ServerType> serverTypeList,List<Server> serverList){
        while (requireList.size()!=0){

            Server s = deployOrPurchase(requireList,serverList,serverTypeList);
            if (s instanceof FakeServer){//表明这是一个部署
                // TODO: 2021/3/23 修改类型强转错误，强转过后不能访问子类信息！
                FakeServer fakeServer = (FakeServer)s;
                //提取出需要部署的请求
                List<Vm> vmList = fakeServer.getDeployedVMList();
                //找到需要部署的实际服务器
                Server server = null;
                for (Server server1:serverList){
                    if (server1.getId()== fakeServer.getId()){
                        server=server1;
                        break;
                    }
                }
                for (Vm vm:vmList) {
                    server.addVM(vm);
                    requireList.removeIf(r -> r.getVmId() == vm.getVmId());
                    dailyOutPut.addDailyDeployVM(vm);
                }

                //将这些需要部署的请求放在VmList中和dailyOutput中
                VmList.addAll(vmList);

            }else if (s instanceof Server){//表明这是一个购买
                Server newServer = s;
                //提取出需要部署的请求
                List<Vm> vmList = newServer.getDeployedVMList();
                //将这个服务器加入到serverList中
                serverList.add(newServer);
                for (Vm vm: vmList){
                    requireList.removeIf(r -> r.getVmId() == vm.getVmId());
                    dailyOutPut.addDailyDeployVM(vm);
                }

                VmList.addAll(vmList);
                //将这个服务器加入到dailyOutPut中
                dailyOutPut.addDailyServer(newServer);

            }

        }

    }

    /**
     * 应该将候选服务器分类，分为已经购买的和题目提供的
     * 遍历完这两个类别的候选服务器，分别排序，分别找到两者剩余资源占比最少的
     * 两者再作比较，找到最最小的
     * 若是新购买的服务器，则新购买，若是已购买的服务器，则只是添加请求
     * @return 如果返回真，表示需要购买新服务器，读取newServer参数，里面会有部署的虚拟机信息；如果返回假，
     */
    static Server deployOrPurchase(List<Require> requireList, List<Server> serverList, List<ServerType> serverTypeList){
        Server newServer;
        FakeServer fakeServer;
        //先模拟出需要处理的请求列表:vmList
        List<Vm> vmList = new ArrayList<>();
        for (Require require:requireList)
            vmList.add(new Vm(require,null,null));
        //再克隆出可以使用的服务器: fakeServerList
        List<FakeServer> fakeServerList = new ArrayList<>();
        for (Server server:serverList)
            fakeServerList.add(new FakeServer(server));
        //提供的服务器假装都能够买
        List<Server> newServerList = new ArrayList<>();
        for (ServerType serverType:serverTypeList)
            newServerList.add(new Server(generateId(),serverType));


        //下面分为两步，分别用serverList和newServerList用来装vmList，装了一个就删除掉，直到装完所有的请求
        for (int i = vmList.size()-1;i>=0;i--){
            Vm vm = vmList.get(i);
            //先遍历FakeServerList
            for (FakeServer fake:fakeServerList){
                String node = isDeployable(vm,fake);
                if (node==null||node.length()==0) continue;
                vm.setNode(node);
                vm.setServer(fake);
                fake.addVM(vm);
                vmList.remove(vm);
                break;//当前假服务器可以装下这个vm，则直接遍历下一个vm
            }
            //再遍历newServerList
            for (Server new_Server:newServerList){
                String node = isDeployable(vm,new_Server);
                if (node==null||node.length()==0) continue;
                vm.setNode(node);
                vm.setServer(new_Server);
                new_Server.addVM(vm);
                vmList.remove(vm);
                break;
            }
//            i = vmList.size();
        }

        //清空两个服务器中没有部署一台虚拟机的服务器
        fakeServerList.removeIf(fakeServer1 -> fakeServer1.getDeployedVMList().size() == 0);
        newServerList.removeIf(newServer1 ->newServer1.getDeployedVMList().size() == 0);


        //对fakeServerList按照剩余资源占比进行升序排序，并取得get(0)，为fakeServer
        fakeServerList.sort(Comparator.comparingDouble(FakeServer::getRestResourceRate));

        //对newServerList按照剩余资源占比进行升序排序，并取得get(0)，为newServer
        newServerList.sort(Comparator.comparingDouble(Server::getRestResourceRate));

        //如果两者中有一个为空，则直接返回其中不为空的get(0)
        if (fakeServerList.size()==0) return newServerList.get(0);
        if (newServerList.size()==0) return fakeServerList.get(0);

        //比较两个get(0)

        // 如果newServer<fakeServer，应该是购买，并对newServer参数设置值,返回true，
        double newServerRestResourceRate = newServerList.get(0).getRestResourceRate();
        double fakeServerRestResourceRate = fakeServerList.get(0).getRestResourceRate();
        if (newServerRestResourceRate<fakeServerRestResourceRate){
            newServer=newServerList.get(0);
            return newServer;
        }
        // 如果fakeServer<=newServer，应该是部署，返回false，并对fakeServer参数设置值
        else{
            fakeServer=fakeServerList.get(0);
            return fakeServer;
        }




    }

    /**
     * 是否可以将虚拟机部署到服务器的某个节点上
     * 单节点部署需要判断能够部署到A还是B节点，双节点部署只需要判断能不能，可以的话返回AB不可以返回null
     * @param VMType
     * @param server
     * @return A B AB null
     */
    static String isDeployable(VmType VMType, Server server){
        int nodeType = VMType.getNodeType();//当前请求双节点或单节点部署
        int requireCPU = VMType.getCPU();//当前请求所需CPU
        int requireMemory = VMType.getMemory();//当前请求所需内存

        if (nodeType==0) {//单节点部署
            if (server.getNodeARestCPU()>=requireCPU&& server.getNodeARestMemory()>=requireMemory) return "A";//部署到A节点
            if (server.getNodeBRestCPU()>=requireCPU&& server.getNodeBRestMemory()>=requireMemory) return "B";//部署到B节点
        }else if (nodeType==1){//双节点部署，每个节点提供需求的一半
            if (server.getNodeARestCPU()>=requireCPU/2&&
                    server.getNodeARestMemory()>=requireMemory/2&&
                    server.getNodeBRestCPU()>=requireCPU/2&&
                    server.getNodeBRestMemory()>=requireMemory/2) return "AB";
        }
        return null;//不能部署，资源不够
    }

    /**
     * 请求的平均核内比
     * @param requireList singleNodeList或doubleNodeList，已经按照cpuAndMemRatio升序排序
     */
    static double averageRequiresRatio(List<Require> requireList){
        double averageVmCoreMemRatio=0;//当天请求的平均核内比
        for (Require require:requireList){
            averageVmCoreMemRatio+=require.getVmType().getCpuAndMemRatio();
        }
        return averageVmCoreMemRatio/requireList.size();
    }

    /**
     * 奖服务器分类为核内比大于一和小于一的
     */
    static void classifyServer(List<ServerType> greaterThanOnes, List<ServerType> lessThanOnes){
        for (ServerType serverType:Input.serverMap.values()){
            if (serverType.getCpuAndMemRatio()>1) greaterThanOnes.add(serverType);
            else lessThanOnes.add(serverType);
        }
    }


    /**
     * 将oneDayList按照 VisualMachine#getNodeType()进行分类，拆成单、双节点两个列表
     * @param addOneDayList 一天的add类型请求
     * @param singleNodeList 当日请求的单节点虚拟机列表
     * @param doubleNodeList 当日请求的双节点虚拟机列表
     */
    static void doubleOrSingle(List<Require> addOneDayList,
                               List<Require> singleNodeList,List<Require> doubleNodeList){
        for(Require require:addOneDayList){
            if (require.getVmType().getNodeType()==0) singleNodeList.add(require);
            else if (require.getVmType().getNodeType()==1) doubleNodeList.add(require);
        }
    }


    /**
     * 产生购买的服务器的编号
     * @return
     */
    static long generateId(){
        return totalId++;
    }


    /**
     * 查看统计数据
     */
    static void showStatistical(){
        //查看统计数据
        long totalCost=0;//记录总花销
        for (Server server : serverList){
            totalCost = Math.addExact(totalCost, server.getHardWareCost());
        }
        for (int i=0;i< outputList.size();i++){
            System.out.println("=============day "+i+"=============");
            Output dailyOutPut=outputList.get(i);
            long dailyRestCpu=0,dailyRestMemory=0;
            long dailyCost=0;
            for (Server server : dailyOutPut.getDailyDeployServer()){
                dailyCost=Math.addExact(dailyCost, server.getDailyCost());
                dailyRestCpu=Math.addExact(dailyRestCpu, server.getNodeARestCPU()+ server.getNodeBRestCPU());
                dailyRestMemory=Math.addExact(dailyRestMemory, server.getNodeARestMemory()+ server.getNodeBRestMemory());
            }
            dailyCost = Math.multiplyExact(dailyCost,outputList.size()-i);
            totalCost=Math.addExact(totalCost,dailyCost);
            System.out.printf("当天购买服务器剩余资源统计：cpu:%d, memory:%d  当天购买的服务器总数%d，当天部署的虚拟机总数%d\n",
                    dailyRestCpu,dailyRestMemory,dailyOutPut.getDailyDeployServer().size(),dailyOutPut.getDailyDeployVM().size());

        }
        System.out.println("总花费："+totalCost);
        int totalRestCpu=0,totalRestMemory=0;
        for (Server server : serverList){//当天运行中服务器剩余资源
            totalRestCpu=Math.addExact(totalRestCpu, server.getNodeARestCPU()+ server.getNodeBRestCPU());
            totalRestMemory=Math.addExact(totalRestMemory, server.getNodeARestMemory()+ server.getNodeBRestMemory());
        }
        System.out.printf("服务器资源总剩余：cpu:%d  memory:%d",totalRestCpu,totalRestMemory);
    }
}
